Skip to content

S01-07 基础-算法基础

[TOC]

排序

排序介绍

排序是将多个数据按指定顺序排列的过程。

排序分类

  1. 内部排序:所有数据加载到内部存储器(内存)中排序,包括:
    • 交换式排序法(如冒泡排序)
    • 选择式排序法
    • 插入式排序法
  2. 外部排序法:数据量过大,无法全部加载到内存,需借助外部存储(如硬盘)排序,包括:
    • 合并排序法
    • 直接合并排序法

冒泡排序

冒泡排序(Bubble Sorting):通过对待排序序列从后向前(下标较大的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部(类似气泡上浮)。

案例:冒泡排序实现

将无序数组{24,69,80,57,13}排成从小到大的有序数列。

思路分析

  1. n 个元素需进行 n-1 轮排序(外层循环次数)。
  2. 每轮排序确定一个元素的最终位置(从最大到最小依次归位)。
  3. 每轮比较次数 = 数组长度 - 1 - 当前轮数(内层循环次数)。
  4. 核心逻辑:相邻元素逆序则交换。

实现步骤

数组初始状态:[24,69,80,57,13]

  • 第 1 轮排序(目标:最大数 80 移到末尾):
    • 比较 24&69(不交换)→ 69&80(不交换)→ 80&57(交换 →[24,69,57,80,13])→ 80&13(交换 →[24,69,57,13,80])
  • 第 2 轮排序(目标:第二大数 69 移到倒数第二):
    • 比较 24&69(不交换)→ 69&57(交换 →[24,57,69,13,80])→ 69&13(交换 →[24,57,13,69,80])
  • 第 3 轮排序(目标:第三大数 57 移到倒数第三):
    • 比较 24&57(不交换)→ 57&13(交换 →[24,13,57,69,80])
  • 第 4 轮排序(目标:第四大数 24 移到倒数第四):
    • 比较 24&13(交换 →[13,24,57,69,80])

image-20251216165414531

代码实现

java
public class BubbleSort {
    public static void main(String[] args) {
        int[] arr = {24, 69, 80, 57, 13, -1, 30, 200, -110};
        int temp = 0; // 辅助交换的变量

        // 外层循环:控制排序轮数(n个元素需n-1轮)
        for (int i = 0; i < arr.length - 1; i++) {
            // 内层循环:每轮比较次数(每轮减少1次,因为末尾已排好序)
            for (int j = 0; j < arr.length - 1 - i; j++) {
                // 相邻元素比较,逆序则交换(从小到大排序)
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }

            // 输出每轮排序结果
            System.out.println("\n==第" + (i+1) + "轮==");
            for (int j = 0; j < arr.length; j++) {
                System.out.print(arr[j] + "\t");
            }
        }
    }
}

查找

在 Java 中,常用的查找方式有两种:

  1. 顺序查找(本章重点)
  2. 二分查找(后续算法讲解)

顺序查找

需求:有一个数列:{"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"},从键盘输入一个名称,判断数列中是否包含该名称。若找到,提示找到并给出下标;若未找到,提示未找到。

思路分析

  1. 定义字符串数组存储目标数列。
  2. 接收用户输入的查找名称。
  3. 遍历数组,逐一比较(字符串比较用equals方法)。
  4. 用变量index标记找到的下标(初始值-1,未找到则保持-1)。

代码实现

java
import java.util.Scanner;

public class SeqSearch {
  public static void main(String[] args) {
    // 目标数组
    String[] names = {"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"};
    Scanner myScanner = new Scanner(System.in);

    System.out.println("请输入名字:");
    String findName = myScanner.next();

    // 查找逻辑
    int index = -1; // 标记下标(未找到为-1)
    for (int i = 0; i < names.length; i++) {
      // 比较输入名称与数组元素
      if (findName.equals(names[i])) {
        System.out.println("恭喜你找到" + findName);
        System.out.println("下标为= " + i);
        index = i; // 更新下标
        break; // 找到后退出循环
      }
    }

    // 未找到的提示
    if (index == -1) {
      System.out.println("sorry,没有找到" + findName);
    }
  }
}

二分查找(有序数组)

二分查找(Binary Search,折半查找):是一种非常高效的搜索算法。它的核心思想是通过不断将搜索区间分成两半,来快速缩小查找范围,最终找到目标值。

前提二分查找只能用于「已经排好序」的数组或集合中。

以下是关于 Java 中二分查找算法的详细介绍。

算法工作原理

二分查找的逻辑非常直观,类似于我们在字典中找一个单词,或者在玩“猜数字”游戏(猜 1 到 100 之间的一个数,每次告诉你猜大了还是猜小了)。

具体步骤如下:

  1. 初始化指针:定义两个指针,left 指向数组起始位置(0),right 指向数组末尾位置(length - 1)。

  2. 计算中间位置:计算 leftright 的中间索引 mid

  3. 比较与移动指针

    • 如果 array[mid] == target,说明找到了目标,直接返回索引 mid
    • 如果 array[mid] < target,说明目标值在右半区间,因此将左指针移动到中间位置的右边:left = mid + 1
    • 如果 array[mid] > target,说明目标值在左半区间,因此将右指针移动到中间位置的左边:right = mid - 1
  4. 循环结束条件:当 left > right 时,说明搜索区间为空,数组中不存在目标值,返回 -1

Java 代码实现

在 Java 中,二分查找通常有两种实现方式:迭代法(While 循环,推荐)递归法。在实际工程中,通常推荐使用迭代法,因为它不会产生额外的函数调用栈开销,且避免了深度递归可能引发的 StackOverflowError

迭代法
java
public class BinarySearch {

    /**
     * 二分查找(迭代版)
     * @param nums   已排序的数组(假设为升序)
     * @param target 需要查找的目标值
     * @return       目标值在数组中的索引,如果未找到则返回 -1
     */
    public static int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }

      	// 初始化指针:left 和 right
        int left = 0;
        int right = nums.length - 1;

        // 注意这里是 left <= right,因为 left 和 right 指向同一个元素时,也需要检查该元素
        while (left <= right) {
            // 重点防坑:使用 left + (right - left) / 2 而不是 (left + right) / 2
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid; // 找到目标值,返回索引
            } else if (nums[mid] < target) {
                left = mid + 1; // 目标值在右侧,缩小左边界
            } else {
                right = mid - 1; // 目标值在左侧,缩小右边界
            }
        }

        return -1; // 循环结束仍未找到,返回 -1
    }

    public static void main(String[] args) {
        int[] arr = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}; // 排好序的数组
        int target = 23;
        int result = search(arr, target);
        System.out.println("目标元素的索引为: " + result); // 输出: 5
    }
}
递归法

递归法:

java
public class BinarySearchRecursive {

    public static int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        return binarySearch(nums, target, 0, nums.length - 1);
    }

    private static int binarySearch(int[] nums, int target, int left, int right) {
        // 递归终止条件
        if (left > right) {
            return -1;
        }

        int mid = left + (right - left) / 2;

        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            // 递归右半区间
            return binarySearch(nums, target, mid + 1, right);
        } else {
            // 递归左半区间
            return binarySearch(nums, target, left, mid - 1);
        }
    }
}
算法复杂度分析

算法复杂度分析:

  • 时间复杂度O(logn)O(\log n)。每次比较后,搜索范围都会减半。这意味着即使在一个包含 40 亿个元素的数组中查找,最多也只需要比较 32 次,效率极高。
  • 空间复杂度
  • 迭代法:O(1)O(1),只需要常数级别的额外空间来存储 leftrightmid 指针。
  • 递归法:O(logn)O(\log n),因为每次递归调用都需要在调用栈上分配空间,最大深度取决于树的高度(即二分的次数)。
核心易错点:Mid 的计算

核心易错点:Mid 的计算:

初学者经常将中间索引计算为:

int mid = (left + right) / 2;

虽然数学上完全正确,但在 Java 编程中这是一个隐藏的 Bug。如果 leftright 都非常大(接近 Integer.MAX_VALUE),它们的和可能会超出 32 位整型的最大值,导致整型溢出,从而变成负数,最终抛出 ArrayIndexOutOfBoundsException

正确的写法是:

int mid = left + (right - left) / 2;

或者使用无符号右移(更高效):

int mid = (left + right) >>> 1; (这也是 Java 官方 Arrays.binarySearch() 源码中采用的写法)。

Java 内置的二分查找 API

Java 内置的二分查找 API:

在实际开发中,你通常不需要手写二分查找,Java 的 java.util.Arraysjava.util.Collections 工具类已经提供了内置方法:

  • 对于数组Arrays.binarySearch(array, target)
  • 对于列表 (List)Collections.binarySearch(list, target)

注意:如果内置 API 找到了元素,它会返回其索引。如果没有找到,它会返回 -(插入点) - 1。这个特性非常有用,它可以告诉你如果要把这个数字插入数组并保持有序,应该插在哪个位置。